package com.zhouyx.redisactive.lock;

import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired;

import com.zhouyx.redisactive.utils.JedisUtils;

/**
 * @ClassName FinalRedisLock
 * @Description 分布式锁之Redis实现（最终版）
 * @Author za-zhouyunxing
 * @Date 2019/4/16 16:17
 * @Version 1.0
 */
public class FinalRedisLock {
    @Autowired
    private JedisUtils jedis;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     *
     * 通过requestId解决了分布式下不同客户端时间不统一问题，
     * 通过超期时间解决了多次getset覆盖问题，通过解锁时判断requestId解决了任何客户端都可以解锁问题。
     *
     * @param lockKey 锁
     * @param requestId 客户端标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {

        //可以看到，我们加锁就一行代码
        // 这个set()方法一共有五个形参
        //第一个为key，我们使用key来当锁，因为key是唯一的
        //第二个为value，我们传的是requestId，requestId是客户端的唯一标志。
        //第三个为nxxx，这个参数我们填的是NX，意思是SET IF NOT EXIST，即当key不存在时，我们进行set操作；若key已经存在，则不做任何操作；
        //第四个为expx，这个参数我们传的是PX，意思是我们要给这个key加一个过期的设置，具体时间由第五个参数决定。
        //第五个为time，与第四个参数相呼应，代表key的过期时间。
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    /**
     * 解锁
     *
     * 这种方式的问题在于如果调用jedis.del()方法的时候，这把锁已经不属于当前客户端的时候会解除他人加的锁
     * 比如客户端A加锁，一段时间之后客户端A解锁，在执行jedis.del()之前，锁突然过期了，
     * 此时客户端B尝试加锁成功，然后客户端A再执行del()方法，则将客户端B的锁给解除了。
     *
     * @param lockKey
     * @param requestId
     */
    public void wrongReleaseLock2(String lockKey, String requestId) {

        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时，这把锁突然不是这个客户端的，则会误解锁
            jedis.del(lockKey);
        }

    }


    /**
     * 释放分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public  boolean releaseDistributedLock(String lockKey, String requestId) {

        //可以看到，我们解锁只需要两行代码就搞定了！
        // 第一行代码，我们写了一个简单的Lua脚本代码,
        // 第二行代码，我们将Lua代码传到jedis.eval()方法里，
        // 并使参数KEYS[1]赋值为lockKey，ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
        //那么这段Lua代码的功能是什么呢？
        //首先获取锁对应的value值，检查是否与requestId相等，如果相等则删除锁（解锁）。
        // 那么为什么要使用Lua语言来实现呢？因为要确保上述操作是原子性的。
        //简单来说，就是在eval命令执行Lua代码的时候，Lua代码将被当成一个命令去执行，并且直到eval命令执行完成，Redis才会执行其他命令
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}
